package com.nus.fellowmark.modules.assignment.service.impl;

import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.google.common.collect.Lists;
import com.nus.fellowmark.entity.GradeCalculationConfigEntity;
import com.nus.fellowmark.entity.ModuleEntity;
import com.nus.fellowmark.entity.PairingEntity;
import com.nus.fellowmark.modules.assignment.constant.GradeConstant;
import com.nus.fellowmark.modules.assignment.constant.PairConstant;
import com.nus.fellowmark.modules.assignment.service.*;
import com.nus.fellowmark.modules.assignment.vo.FinalGradeVO;
import com.nus.fellowmark.modules.assignment.vo.GradeAsgVO;
import com.nus.fellowmark.modules.module.service.EnrollmentService;
import com.nus.fellowmark.modules.module.service.ModuleService;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nus.fellowmark.common.utils.PageUtils;
import com.nus.fellowmark.common.utils.Query;

import com.nus.fellowmark.dao.FinalGradeDao;
import com.nus.fellowmark.entity.FinalGradeEntity;


@Service("finalGradeService")
public class FinalGradeServiceImpl extends ServiceImpl<FinalGradeDao, FinalGradeEntity> implements FinalGradeService {

    @Autowired
    AssignmentService assignmentService;

    @Autowired
    ModuleService moduleService;

    @Autowired
    EnrollmentService enrollmentService;

    @Autowired
    PairingService pairingService;

    @Autowired
    GradeService gradeService;

    @Autowired
    GradeCalculationConfigService configService;

    @Autowired
    RubricService rubricService;

    @Autowired
    FinalGradeDao finalGradeDao;


    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<FinalGradeEntity> page = this.page(
                new Query<FinalGradeEntity>().getPage(params),
                new QueryWrapper<FinalGradeEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    @GlobalTransactional
    public List<FinalGradeVO> calculateFinalGrade(Integer assignmentId) {
        // get config of this assignmentId
        GradeCalculationConfigEntity config = configService.getGradeConfig(assignmentId);
        // sum up rubric max marks for all the questions in this assignment.
        float fullMark = rubricService.getFullMarkOfAssignment(assignmentId);
        // fullMark should not be less or equal to 0
        if (fullMark <= 0) {
            return null;
        }

        List<FinalGradeVO> res = new LinkedList<>();

        // assignmentId -> moduleId -> studentIds (find all the student under this assignment)
        ModuleEntity moduleEntity = moduleService.getByAssignmentId(assignmentId);
        if (moduleEntity == null) {
            return Lists.newLinkedList();
        }
        Integer moduleId = moduleEntity.getId();
        List<Integer> stuIds = enrollmentService.getStudentIdsInModule(moduleId);

        // get all the grades in the assignment
        List<GradeAsgVO> gradeAsgVOS = gradeService.listGradeByAssignmentId(assignmentId);
        // group by studentId & submissionId
        Map<Integer, Map<Integer, List<GradeAsgVO>>> stu2pair2GradeMap = gradeAsgVOS.stream()
                .collect(Collectors.groupingBy(
                        GradeAsgVO::getStudentId, // First-level grouping by studentId
                        Collectors.groupingBy(GradeAsgVO::getPairingId) // Second-level grouping by paringId
                ));

        // for each student, find pairings where the status active (means his/her peer has graded the submission)
        // -> find the grades, sum up grades in one (submissionId & pairId),
        // -> calculate participation marks
        // -> save into the db (add or update)
        for (Integer stuId : stuIds) {
            float finalGrade = 0;
            // calculate single student grade
            List<Double> grades = calculateGradeByStuId(assignmentId, stu2pair2GradeMap.get(stuId), stuId);

            // (1) If exclude highest / lowest marks (only when no. of peers >= 3, can this rule be applied)
            if (grades.size() >= 3) {
                if (!config.getIncludeHighest().equals(GradeConstant.CONFIG_INCLUDE_HIGHEST)) {
                    grades.remove(grades.size() - 1);
                }
                if (!config.getIncludeLowest().equals(GradeConstant.CONFIG_INCLUDE_LOWEST)) {
                    grades.remove(0);
                }
            }
            // average grade
            float averageGrade = (float) grades.stream()
                    .mapToDouble(Double::doubleValue)
                    .average()
                    .orElse(0.0); // if list is null, return 0.0
            // make the grade into percentage: grade / sum(maxMarks)
            float percentageGrade = averageGrade / fullMark;

            // (2) If consider participation: x% * sum(grades) / sum(gradedNum) + (1-x%) * (taskAssignedNum / taskFinishedNum)
            float participationRate = 0;
            if (config.getIncludeParticipation().equals(GradeConstant.CONFIG_INCLUDE_PARTICIPATION)) {
                participationRate = calculateParticipationRate(stuId, assignmentId);
                Double ptcWeight = config.getPtcWeight();
                finalGrade = (float) (ptcWeight * participationRate + (1 - ptcWeight) * percentageGrade);
            } else {
                finalGrade = percentageGrade;
            }
            finalGrade *= fullMark;

            // record peer_grades as string
            String gradesString = grades.stream()
                    .map(Object::toString)
                    .collect(Collectors.joining(", "));

            // compact result
            FinalGradeEntity finalGradeEntity = new FinalGradeEntity();
            finalGradeEntity.setStuId(stuId);
            finalGradeEntity.setAssignmentId(assignmentId);
            finalGradeEntity.setActualGrade(averageGrade);
            finalGradeEntity.setPercentageGrade(percentageGrade);
            finalGradeEntity.setParticipationGrade(participationRate);
            finalGradeEntity.setFinalGrade(finalGrade);
            finalGradeEntity.setPeerGrades(gradesString);

            // add or update into the database
            boolean success = addOrUpdateFinalGradeEntity(finalGradeEntity);

            FinalGradeVO finalGradeVO = new FinalGradeVO();
            BeanUtils.copyProperties(finalGradeEntity, finalGradeVO);
            res.add(finalGradeVO);
        }
        return res;
    }

    private boolean addOrUpdateFinalGradeEntity(FinalGradeEntity finalGradeEntity) {
        FinalGradeEntity entity = finalGradeDao.selectOne(new QueryWrapper<FinalGradeEntity>().eq("stu_id", finalGradeEntity.getStuId()).eq("assignment_id", finalGradeEntity.getAssignmentId()));
        boolean success;
        if (entity != null) {
            success = finalGradeDao.update(finalGradeEntity,
                    new UpdateWrapper<FinalGradeEntity>().eq("stu_id",finalGradeEntity.getStuId()).eq("assignment_id", finalGradeEntity.getAssignmentId())) > 0;
        } else {
            success = finalGradeDao.insert(finalGradeEntity) > 0;
        }
        return success;
    }

    private float calculateParticipationRate(Integer stuId, Integer assignmentId) {
        List<PairingEntity> assignedTasks = pairingService.listPairsByMarkerAndAsgId(stuId, assignmentId);
        int total = assignedTasks.size();
        int count = (int) assignedTasks.stream().filter(task -> task.getActive().equals(PairConstant.ACTIVE)).count();
        return (float) count / total;
    }

    private List<Double> calculateGradeByStuId(Integer assignmentId, Map<Integer, List<GradeAsgVO>> pair2GradeMap, Integer stuId) {
        List<PairingEntity> pairingEntities = pairingService.listPairsByStuAndAsgId(stuId, assignmentId);
        List<PairingEntity> pairsGraded = pairingEntities.stream().filter(pairingEntity -> pairingEntity.getActive().equals(1)).collect(Collectors.toList());
        List<Double> peerGrades = new LinkedList<>();
        for (PairingEntity pairingEntity : pairsGraded) {
            Integer pairId = pairingEntity.getId();
            List<GradeAsgVO> gradeAsgVOList = pair2GradeMap.get(pairId);

            // in order to avoid duplicate submission, only count the final submission
            Integer max = gradeAsgVOList.stream()
                    .map(GradeAsgVO::getSubmissionId)
                    .max(Comparator.comparingInt(sub -> sub))
                    .orElse(0);

            // grade is for one question, sum up to the grade of the assignment
            double grade = gradeAsgVOList.stream()
                    .filter(gradeAsgVO -> gradeAsgVO.getSubmissionId().equals(max))
                    .mapToDouble(GradeAsgVO::getGrade)
                    .sum();
            peerGrades.add(grade);
        }
        peerGrades.sort(Double::compareTo);
        return peerGrades;
    }
}